#[cfg(all(target_arch = "wasm32", feature = "web-csr"))]
use std::time::Duration;

#[cfg(all(target_arch = "wasm32", feature = "web-csr"))]
use tracing::instrument;
#[cfg(all(target_arch = "wasm32", feature = "web-csr"))]
use wasm_bindgen::{closure::Closure, JsCast, JsValue, UnwrapThrowExt};

#[cfg(all(target_arch = "wasm32", feature = "web-csr"))]
use crate::{
    node::Node,
    web::{
        events::{self, EventDescriptor},
        window,
    },
};

mod event;
pub use event::*;

/// Sets a property on a DOM element.
#[cfg(all(target_arch = "wasm32", feature = "web-csr"))]
pub fn set_property(el: &web_sys::Element, prop_name: &str, value: &Option<JsValue>) {
    let key = JsValue::from_str(prop_name);
    match value {
        Some(value) => _ = js_sys::Reflect::set(el, &key, value),
        None => _ = js_sys::Reflect::delete_property(el, &key),
    };
}

/// Gets the value of a property set on a DOM element.
#[cfg(all(target_arch = "wasm32", feature = "web-csr"))]
pub fn get_property(el: &web_sys::Element, prop_name: &str) -> Result<JsValue, JsValue> {
    let key = JsValue::from_str(prop_name);
    js_sys::Reflect::get(el, &key)
}

/// Returns the current [`window.location`](https://developer.mozilla.org/en-US/docs/Web/API/Window/location).
#[cfg(all(target_arch = "wasm32", feature = "web-csr"))]
pub fn location() -> web_sys::Location {
    window().location()
}

/// Current [`window.location.hash`](https://developer.mozilla.org/en-US/docs/Web/API/Window/location)
/// without the beginning #.
#[cfg(all(target_arch = "wasm32", feature = "web-csr"))]
pub fn location_hash() -> Option<String> {
    if crate::is_server() {
        None
    } else {
        location().hash().ok().map(|hash| hash.replace('#', ""))
    }
}

// /// Current [`window.location.pathname`](https://developer.mozilla.org/en-US/docs/Web/API/Window/location).
#[cfg(all(target_arch = "wasm32", feature = "web-csr"))]
pub fn location_pathname() -> Option<String> {
    location().pathname().ok()
}

/// Helper function to extract [`Event.target`](https://developer.mozilla.org/en-US/docs/Web/API/Event/target)
/// from any event.
#[cfg(all(target_arch = "wasm32", feature = "web-csr"))]
pub fn event_target<T>(event: &web_sys::Event) -> T
where
    T: JsCast,
{
    event.target().unwrap_throw().unchecked_into::<T>()
}

/// Helper function to extract `event.target.value` from an event.
///
/// This is useful in the `on:input` or `on:change` listeners for an `<input>` element.
#[cfg(all(target_arch = "wasm32", feature = "web-csr"))]
pub fn event_target_value<T>(event: &T) -> String
where
    T: JsCast,
{
    event
        .unchecked_ref::<web_sys::Event>()
        .target()
        .unwrap_throw()
        .unchecked_into::<web_sys::HtmlInputElement>()
        .value()
}

/// Helper function to extract `event.target.checked` from an event.
///
/// This is useful in the `on:change` listeners for an `<input type="checkbox">` element.

#[cfg(all(target_arch = "wasm32", feature = "web-csr"))]
pub fn event_target_checked(ev: &web_sys::Event) -> bool {
    ev.target().unwrap().unchecked_into::<web_sys::HtmlInputElement>().checked()
}

/// Handle that is generated by [request_animation_frame_with_handle] and can
/// be used to cancel the animation frame request.
#[cfg(all(target_arch = "wasm32", feature = "web-csr"))]
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct AnimationFrameRequestHandle(i32);

#[cfg(all(target_arch = "wasm32", feature = "web-csr"))]
impl AnimationFrameRequestHandle {
    /// Cancels the animation frame request to which this refers.
    /// See [`cancelAnimationFrame()`](https://developer.mozilla.org/en-US/docs/Web/API/Window/cancelAnimationFrame)
    pub fn cancel(&self) {
        #[cfg(all(target_arch = "wasm32", feature = "web-csr"))]
        {
            _ = window().cancel_animation_frame(self.0);
        }
    }
}

/// Runs the given function between the next repaint using
/// [`Window.requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame).
#[cfg(all(target_arch = "wasm32", feature = "web-csr"))]
#[cfg_attr(debug_assertions, instrument(level = "trace", skip_all))]
#[inline(always)]
pub fn request_animation_frame(cb: impl FnOnce() + 'static) {
    _ = request_animation_frame_with_handle(cb);
}

/// Runs the given function between the next repaint using
/// [`Window.requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame),
/// returning a cancelable handle.
#[cfg(all(target_arch = "wasm32", feature = "web-csr"))]
#[cfg_attr(debug_assertions, instrument(level = "trace", skip_all))]
#[inline(always)]
pub fn request_animation_frame_with_handle(cb: impl FnOnce() + 'static) -> Result<AnimationFrameRequestHandle, JsValue> {
    cfg_if! {
      if #[cfg(debug_assertions)] {
        let span = ::tracing::Span::current();
        let cb = move || {
          let _guard = span.enter();
          cb();
        };
      }
    }

    #[inline(never)]
    fn raf(cb: JsValue) -> Result<AnimationFrameRequestHandle, JsValue> {
        window()
            .request_animation_frame(cb.as_ref().unchecked_ref())
            .map(AnimationFrameRequestHandle)
    }

    raf(Closure::once_into_js(cb))
}

/// Handle that is generated by [request_idle_callback_with_handle] and can be
/// used to cancel the idle callback.
#[cfg(all(target_arch = "wasm32", feature = "web-csr"))]
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct IdleCallbackHandle(u32);

#[cfg(all(target_arch = "wasm32", feature = "web-csr"))]
impl IdleCallbackHandle {
    /// Cancels the idle callback to which this refers.
    /// See [`cancelAnimationFrame()`](https://developer.mozilla.org/en-US/docs/Web/API/Window/cancelIdleCallback)
    pub fn cancel(&self) {
        window().cancel_idle_callback(self.0);
    }
}

/// Queues the given function during an idle period using
/// [`Window.requestIdleCallback`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestIdleCallback).
#[cfg(all(target_arch = "wasm32", feature = "web-csr"))]
#[cfg_attr(debug_assertions, instrument(level = "trace", skip_all))]
#[inline(always)]
pub fn request_idle_callback(cb: impl Fn() + 'static) {
    _ = request_idle_callback_with_handle(cb);
}

/// Queues the given function during an idle period using
/// [`Window.requestIdleCallback`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestIdleCallback),
/// returning a cancelable handle.
#[cfg(all(target_arch = "wasm32", feature = "web-csr"))]
#[cfg_attr(debug_assertions, instrument(level = "trace", skip_all))]
#[inline(always)]
pub fn request_idle_callback_with_handle(cb: impl Fn() + 'static) -> Result<IdleCallbackHandle, JsValue> {
    cfg_if! {
      if #[cfg(debug_assertions)] {
        let span = ::tracing::Span::current();
        let cb = move || {
          let _guard = span.enter();
          cb();
        };
      }
    }

    #[inline(never)]
    fn ric(cb: Box<dyn Fn()>) -> Result<IdleCallbackHandle, JsValue> {
        let cb = Closure::wrap(cb).into_js_value();

        window().request_idle_callback(cb.as_ref().unchecked_ref()).map(IdleCallbackHandle)
    }

    ric(Box::new(cb))
}

/// Handle that is generated by [set_timeout_with_handle] and can be used to clear the timeout.
#[cfg(all(target_arch = "wasm32", feature = "web-csr"))]
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct TimeoutHandle(i32);

#[cfg(all(target_arch = "wasm32", feature = "web-csr"))]
impl TimeoutHandle {
    /// Cancels the timeout to which this refers.
    /// See [`clearTimeout()`](https://developer.mozilla.org/en-US/docs/Web/API/clearTimeout)
    pub fn clear(&self) {
        #[cfg(all(target_arch = "wasm32", feature = "web-csr"))]
        window().clear_timeout_with_handle(self.0);
    }
}

/// Executes the given function after the given duration of time has passed, returning a cancelable handle.
/// [`setTimeout()`](https://developer.mozilla.org/en-US/docs/Web/API/setTimeout).
#[cfg(all(target_arch = "wasm32", feature = "web-csr"))]
#[cfg_attr(
  any(debug_assertions, feature = "web-ssr"),
  instrument(level = "trace", skip_all, fields(duration = ?duration))
)]
#[inline(always)]
pub fn set_timeout(cb: impl FnOnce() + 'static, duration: Duration) -> Result<TimeoutHandle, JsValue> {
    cfg_if! {
      if #[cfg(debug_assertions)] {
        let span = ::tracing::Span::current();
        let cb = move || {
          let _guard = span.enter();
          cb();
        };
      }
    }

    #[inline(never)]
    fn st(cb: JsValue, duration: Duration) -> Result<TimeoutHandle, JsValue> {
        window()
            .set_timeout_with_callback_and_timeout_and_arguments_0(cb.as_ref().unchecked_ref(), duration.as_millis().try_into().unwrap_throw())
            .map(TimeoutHandle)
    }
    st(Closure::once_into_js(cb), duration)
}
/// Handle that is generated by [set_interval] and can be used to clear the interval.
#[cfg(all(target_arch = "wasm32", feature = "web-csr"))]
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct IntervalHandle(i32);

#[cfg(all(target_arch = "wasm32", feature = "web-csr"))]
impl IntervalHandle {
    /// Cancels the repeating event to which this refers.
    /// See [`clearInterval()`](https://developer.mozilla.org/en-US/docs/Web/API/clearInterval)
    pub fn clear(&self) {
        window().clear_interval_with_handle(self.0);
    }
}

/// Repeatedly calls the given function, with a delay of the given duration between calls,
/// returning a cancelable handle.
/// See [`setInterval()`](https://developer.mozilla.org/en-US/docs/Web/API/setInterval).
#[cfg(all(target_arch = "wasm32", feature = "web-csr"))]
#[inline(always)]
pub fn set_interval(cb: impl Fn() + 'static, duration: Duration) -> Result<IntervalHandle, JsValue> {
    cfg_if! {
      if #[cfg(debug_assertions)] {
        let span = ::tracing::Span::current();
        let cb = move || {
          let _guard = span.enter();
          cb();
        };
      }
    }
    let cb = Closure::wrap(Box::new(cb) as Box<dyn Fn()>).into_js_value();
    let handle = window()
        .set_interval_with_callback_and_timeout_and_arguments_0(cb.as_ref().unchecked_ref(), duration.as_millis().try_into().unwrap_throw())?;
    Ok(IntervalHandle(handle))
}

/// Adds an event listener to the `Window`, typed as a generic `Event`.
#[cfg(all(target_arch = "wasm32", feature = "web-csr"))]
#[cfg_attr(
  debug_assertions,
  instrument(level = "trace", skip_all, fields(event_name = %event_name))
)]
#[inline(always)]
pub fn window_event_listener_untyped(event_name: &str, cb: impl Fn(web_sys::Event) + 'static) {
    cfg_if! {
      if #[cfg(debug_assertions)] {
        let span = ::tracing::Span::current();
        let cb = move |e| {
          let _guard = span.enter();
          cb(e);
        };
      }
    }

    if !crate::is_server() {
        #[inline(never)]
        fn wel(cb: Box<dyn FnMut(web_sys::Event)>, event_name: &str) {
            let cb = Closure::wrap(cb).into_js_value();
            _ = window().add_event_listener_with_callback(event_name, cb.unchecked_ref());
        }

        wel(Box::new(cb), event_name);
    }
}

/// Creates a window event listener from a typed event.
#[cfg(all(target_arch = "wasm32", feature = "web-csr"))]
pub fn window_event_listener<E: EventDescriptor + 'static>(event: E, cb: impl Fn(E::EventType) + 'static)
where
    E::EventType: JsCast,
{
    window_event_listener_untyped(&event.name(), move |e| cb(e.unchecked_into::<E::EventType>()));
}

#[cfg(all(target_arch = "wasm32", feature = "web-csr"))]
#[doc(hidden)]
/// This exists only to enable type inference on event listeners when in SSR mode.
pub fn ssr_event_listener<E: events::EventDescriptor + 'static>(_event: E, _event_handler: impl FnMut(E::EventType) + 'static) {}

/// "Debounce" a callback function. This will cause it to wait for a period of `delay`
/// after it is called. If it is called again during that period, it will wait
/// `delay` before running, and so on. This can be used, for example, to wrap event
/// listeners to prevent them from firing constantly as you type.
#[cfg(all(target_arch = "wasm32", feature = "web-csr"))]
pub fn debounce<T: 'static>(
    delay: Duration,
    #[cfg(debug_assertions)] mut cb: impl FnMut(T) + 'static,
    #[cfg(not(debug_assertions))] cb: impl FnMut(T) + 'static,
) -> impl FnMut(T) {
    use std::{
        cell::{Cell, RefCell},
        rc::Rc,
    };

    cfg_if! {
      if #[cfg(debug_assertions)] {
        let span = ::tracing::Span::current();
        let cb = move |value| {
          let _guard = span.enter();
          cb(value);
        };
      }
    }

    let cb = Rc::new(RefCell::new(cb));

    let timer = Rc::new(Cell::new(None::<TimeoutHandle>));

    move |arg| {
        if let Some(timer) = timer.take() {
            timer.clear();
        }
        let handle = set_timeout(
            {
                let cb = Rc::clone(&cb);
                move || {
                    cb.borrow_mut()(arg);
                }
            },
            delay,
        );
        if let Ok(handle) = handle {
            timer.set(Some(handle));
        }
    }
}

#[cfg(all(target_arch = "wasm32", feature = "web-csr"))]
pub fn nodes_between(first: &Node, last: &Node) -> Vec<web_sys::Node> {
    let mut nodes = vec![];
    let mut current = Some(<web_sys::Element as AsRef<web_sys::Node>>::as_ref(first).clone());
    while let Some(node) = current {
        nodes.push(node.clone());
        if node == *last.as_ref() {
            break;
        }
        current = node.next_sibling();
    }
    nodes
}
